package com.arpa.wms.handler;

import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.arpa.ntocc.common.common.exception.ServiceException;
import com.arpa.wms.domain.dto.AuthDto;
import com.arpa.wms.domain.entity.Shop;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.gson.*;
import com.google.gson.internal.LinkedTreeMap;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static com.arpa.wms.handler.AbstractTokenManagerHandler.DY_TOKEN_MANAGER;


/**
 * 抖音token处理
 *
 * @author lxl
 * @since 2021-09-16
 */
@Log4j2
@Component(DY_TOKEN_MANAGER)
public class DyTokenBasicHandler extends AbstractTokenManagerHandler {


    private static final Gson CustomGson = new GsonBuilder()
            .registerTypeAdapter(LinkedTreeMap.class, newMapSerializer())
            // 定制数值类型的序列化，确保整数输出不带小数点
            .registerTypeAdapter(Integer.class, newNumberSerializer())
            .registerTypeAdapter(Long.class, newNumberSerializer())
            .registerTypeAdapter(Double.class, newNumberSerializer())
            .disableHtmlEscaping() // 禁用Html Escape，确保符号不转义：&<>='
            .create();

    String host = "https://openapi.jinritemai.com";
    String appKey = "6995344980931790370";
    String appSecret = "6788d817-1a16-4acc-b6bd-633261a2a9c4";

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void refreshToken(Shop shop) {
        log.info("抖音token刷新处理。{}", JSONUtil.toJsonStr(shop));
        String method = "token.refresh";
        long timestamp = System.currentTimeMillis() / 1000;
        Map<String, Object> m = new HashMap<>();
        m.put("refresh_token", shop.getRefreshToken());
        m.put("grant_type", "refresh_token");
        // 序列化参数
        String paramJson = marshal(m);
        // 计算签名
        String signVal = sign(appKey, appSecret, method, timestamp, paramJson);
        String methodPath = method.replace('.', '/');
        String u = host + "/" + methodPath +
                "?method=" + HttpUtil.encodeParams(method, StandardCharsets.UTF_8) +
                "&app_key=" + HttpUtil.encodeParams(appKey, StandardCharsets.UTF_8) +
                "&timestamp=" + HttpUtil.encodeParams(Long.toString(timestamp), StandardCharsets.UTF_8) +
                "&v=" + HttpUtil.encodeParams("2", StandardCharsets.UTF_8) +
                "&sign=" + HttpUtil.encodeParams(signVal, StandardCharsets.UTF_8);
        Map<String, Object> params = new HashMap<>();
        params.put("param_json", paramJson);
        String responseVal = HttpUtil.post(u, params);
        JSONObject temp = JSONUtil.parseObj(responseVal);
        JSONObject data = (JSONObject) temp.get("data");
        int expiresIn = (int) data.get("expires_in");
        DateTime gmtTokenExp = DateUtil.offset(DateUtil.date(), DateField.SECOND, expiresIn);
        String accessToken = (String) data.get("access_token");
        log.info("抖音token刷新处理，结果：{}",responseVal);
        if (!StrUtil.equals(accessToken, shop.getAccessToken())) {
            log.info("抖音token刷新处理，更新token，{}",accessToken);
            String refreshToken = (String) data.get("refresh_token");
            shop.setGmtTokenExp(gmtTokenExp);
            shop.setAccessToken(accessToken);
            shop.setRefreshToken(refreshToken);
            Set<String> carrierCodes = shopMapper.getCarrierCode(shop.getCode());

            log.info("抖音token刷新处理，更新token，清除承运商缓存,{}",JSONUtil.toJsonStr(carrierCodes));
            carrierCache.flushCaches(carrierCodes);
            log.info("抖音token刷新处理，更新token，清除店铺缓存,{}",shop.getCode());
            shopCache.flushCaches(shop.getCode(),shop.getShipmentCode());
            shopMapper.updateById(shop);
            log.info("抖音token刷新处理，更新token，更新店铺TOKEN,{}",shop.getCode());
            shopMapper.updateCarrierToken(accessToken, shop.getCode());
            log.info("抖音token刷新处理，更新token，更新承运商TOKEN,{}",shop.getCode());
        }
    }

    /**
     * 获取token
     *
     * @param dto 授权信息
     * @author liuxr
     * @date 2021/10/25 17:19
     */
    @Override
    public void getToken(AuthDto dto) {

        String method = "token.create";
        long timestamp = System.currentTimeMillis() / 1000;
        Map<String, Object> m = new HashMap<>();
        m.put("code", dto.getCode());
        m.put("grant_type", "authorization_code");
        // 序列化参数
        String paramJson = marshal(m);
        System.out.println("param_json:" + paramJson);
        // 计算签名
        String signVal = sign(appKey, appSecret, method, timestamp, paramJson);
        String methodPath = method.replace('.', '/');
        String u = host + "/" + methodPath +
                "?method=" + HttpUtil.encodeParams(method, StandardCharsets.UTF_8) +
                "&app_key=" + HttpUtil.encodeParams(appKey, StandardCharsets.UTF_8) +
                "&timestamp=" + HttpUtil.encodeParams(Long.toString(timestamp), StandardCharsets.UTF_8) +
                "&v=" + HttpUtil.encodeParams("2", StandardCharsets.UTF_8) +
                "&sign=" + HttpUtil.encodeParams(signVal, StandardCharsets.UTF_8);
        Map<String, Object> params = new HashMap<>();
        params.put("param_json", paramJson);
        String responseVal = HttpUtil.post(u, params);
        JSONObject temp = JSONUtil.parseObj(responseVal);
        int errNo = (int) temp.get("err_no");
        if (errNo != 0) {
            String message = (String) temp.get("message");
            log.error(message);
            throw new ServiceException(message);
        }

        JSONObject data = (JSONObject) temp.get("data");
        int expiresIn = (int) data.get("expires_in");
        DateTime gmtTokenExp = DateUtil.offset(DateUtil.date(), DateField.SECOND, expiresIn);
        String accessToken = (String) data.get("access_token");
        String refreshToken = (String) data.get("refresh_token");
        int shop_id = (int) data.get("shop_id");
        Shop shop = shopMapper.selectOne(Wrappers.lambdaQuery(Shop.class).eq(Shop::getUserId, shop_id + "").last("limit 1"));
        if (null == shop) {
            log.error("授权失败，系统中找不到对应的店铺信息。userId:{}", shop_id);
            throw new ServiceException("授权失败，系统中找不到对应的店铺信息");
        }
        shop.setAccessToken(accessToken);
        shop.setRefreshToken(refreshToken);
        shop.setGmtTokenExp(gmtTokenExp);
        shopMapper.updateById(shop);

    }


    /**
     * 调用Open Api，取回数据
     *
     * @param appKey
     * @param host
     * @param method
     * @param timestamp
     * @param paramJson
     * @param accessToken
     * @param sign
     * @return java.lang.String
     * @author liuxr
     * @date 2021/10/25 14:06
     */
    public static String fetch(String appKey, String host, String method, long timestamp, String paramJson, String accessToken, String sign) throws IOException {
        String methodPath = method.replace('.', '/');

        String u = host + "/" + methodPath +
                "?method=" + HttpUtil.encodeParams(method, StandardCharsets.UTF_8) +
                "&app_key=" + HttpUtil.encodeParams(appKey, StandardCharsets.UTF_8) +
                "&access_token=" + HttpUtil.encodeParams(accessToken, StandardCharsets.UTF_8) +
                "&timestamp=" + HttpUtil.encodeParams(Long.toString(timestamp), StandardCharsets.UTF_8) +
                "&v=" + HttpUtil.encodeParams("2", StandardCharsets.UTF_8) +
                "&sign=" + HttpUtil.encodeParams(sign, StandardCharsets.UTF_8);

        Map<String, Object> params = new HashMap<>();
        params.put("param_json", paramJson);
        return HttpUtil.post(u, params);
    }

    /**
     * 计算签名
     *
     * @param appKey
     * @param appSecret
     * @param method
     * @param timestamp
     * @param paramJson
     * @return java.lang.String
     * @author liuxr
     * @date 2021/10/25 14:04
     */
    public static String sign(String appKey, String appSecret, String method, long timestamp, String paramJson) {
        // 按给定规则拼接参数
        String paramPattern = "app_key" + appKey + "method" + method + "param_json" + paramJson + "timestamp" + timestamp + "v2";
        String signPattern = appSecret + paramPattern + appSecret;
        System.out.println("sign_pattern:" + signPattern);

        try {
            return stringToMD5(signPattern);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return hmac(signPattern, appSecret);
    }

    public static String stringToMD5(String plainText) throws UnsupportedEncodingException {
        byte[] secretBytes = null;
        try {
            secretBytes = MessageDigest.getInstance("md5").digest(
                    plainText.getBytes("UTF-8"));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("没有这个md5算法！");
        }
        StringBuilder md5code = new StringBuilder(new BigInteger(1, secretBytes).toString(16));
        while (md5code.length() < 32) {
            md5code.insert(0, "0");
        }
        return md5code.toString();
    }

    /**
     * 计算hmac
     *
     * @param plainText
     * @param appSecret
     * @return java.lang.String
     * @author liuxr
     * @date 2021/10/25 14:04
     */
    public static String hmac(String plainText, String appSecret) {
        Mac mac;
        try {
            byte[] secret = appSecret.getBytes(StandardCharsets.UTF_8);
            SecretKeySpec keySpec = new SecretKeySpec(secret, "HmacSHA256");

            mac = Mac.getInstance("HmacSHA256");
            mac.init(keySpec);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            return "";
        }

        byte[] plainBytes = plainText.getBytes(StandardCharsets.UTF_8);
        byte[] digest = mac.doFinal(plainBytes);
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    /**
     * 序列化参数
     * 这一步看上去冗余，实际很重要。如果要自己实现，则必须保证这三点：
     * 1、保证JSON所有层级上Key的有序性
     * 2、保证JSON的所有数值不带多余的小数点
     * 3、保证转义逻辑与这段代码一致
     *
     * @param o
     * @return java.lang.String
     * @author liuxr
     * @date 2021/10/25 14:04
     */
    public static String marshal(Object o) {
        String raw = CustomGson.toJson(o);
        // 执行反序列化，把所有JSON对象转换成LinkedTreeMap
        Map<?, ?> m = CustomGson.fromJson(raw, LinkedTreeMap.class);
        // 重新序列化，保证JSON所有层级上Key的有序性
        return CustomGson.toJson(m);
    }


    /**
     * 为LinkedTreeMap定制的序列化器
     *
     * @return com.google.gson.JsonSerializer<com.google.gson.internal.LinkedTreeMap < ?, ?>>
     * @author liuxr
     * @date 2021/10/25 14:04
     */
    public static JsonSerializer<LinkedTreeMap<?, ?>> newMapSerializer() {
        return new JsonSerializer<LinkedTreeMap<?, ?>>() {
            @Override
            public JsonElement serialize(LinkedTreeMap<?, ?> src, Type typeOfSrc, JsonSerializationContext context) {
                List<String> keys = src.keySet().stream().map(Object::toString).sorted().collect(Collectors.toList());
                JsonObject o = new JsonObject();
                for (String k : keys) {
                    o.add(k, context.serialize(src.get(k)));
                }
                return o;
            }
        };
    }

    /**
     * 为Number定制化的序列化器
     *
     * @return com.google.gson.JsonSerializer<T>
     * @author liuxr
     * @date 2021/10/25 14:05
     */
    private static <T extends Number> JsonSerializer<T> newNumberSerializer() {
        return new JsonSerializer<T>() {
            @Override
            public JsonElement serialize(T number, Type type, JsonSerializationContext context) {
                if (number instanceof Integer) {
                    return new JsonPrimitive(number.intValue());
                }
                if (number instanceof Long) {
                    return new JsonPrimitive(number.longValue());
                }
                if (number instanceof Double) {
                    long longValue = number.longValue();
                    double doubleValue = number.doubleValue();
                    if (longValue == doubleValue) {
                        return new JsonPrimitive(longValue);
                    }
                }
                return new JsonPrimitive(number);
            }
        };
    }
}


